<?php
/*
 * Copyright (C) 2006-2007 by Openmoko, Inc.
 * Written by Lukasz Stosik <lstosik@gmail.com>
 * All Rights Reserved
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */
require_once 'MicroTal/Repeat.php';
require_once 'MicroTal/CompilingContext.php';
class MicroTal {
	/** @var string absolute path to directory with compiled templates */
	public $tempDir__;
	/** @var string absolute path to directory with source template files */
	public $templateDir__;
	/** @var string encoding used for file input/output */
	public $encoding__ = 'utf8';
	/** @var boolean is cache enabled, default true */
	public $cacheEnabled__ = false;
	private $__vars = array();
	private $__template;
	protected $__template_stack;
	/** @var MicroTalContext */
	protected $__context;
	protected $__context_stack;
	protected $__includes = array();
	protected $__included = array();
	/** @link http://stackoverflow.com/questions/97522?sort=newest#sort-top */
	protected $selfClosing = array('area','base','basefont', 'br', 'hr', 'input', 'img', 'link', 'meta');
	function __construct($templateDir = null) {
		$this->__template = '';
		$this->__template_stack = array();
		if ($templateDir == null) {
			$this->templateDir__ = $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR . 'tpl';
		} else {
			$this->templateDir__ = $templateDir;
		}
		$this->tempDir__ = sys_get_temp_dir();
	}
	protected function tal_repeat($value, $context) {
		static $prefixPattern = array(
		'$__array = %1',
		'$repeat = $_r->getRepeat();',
		'$repeat["%0"] = new MicroTal_Repeat($__array)',
		'foreach( $__array as $repeat["%0"]->key => $_r->%0):',
		);
		static $sufixPattern = array(
		'$repeat["%0"]->iterate()',
		'endforeach',
		'unset($repeat["%0"])'
		);
		list($item,$path) = explode(' ',$value,2);
		$context->prependPhp($prefixPattern, array($item, $this->parseTalesExpression($path)));
		/*$context->prepend(MicrotalBuilder::exec(
			array($item, $this->parseTalesExpression($path)),
			$prefixPattern
		));*/
		//$context->append(MicrotalBuilder::exec($item, $sufixPattern));
		$context->appendPhp($sufixPattern, $item);
	}
	protected function tal_replace($value, $context) {
		$this->tal_content($value, $context);
		$context->omit = true;
	}
	protected function tal_content($value, $context) {
		$escape = true;
		if ($this->checkKeyword($value, 'structure ')) {
			$escape = false;
		}
		if ($this->checkKeyword($value, 'text ')) {
			$escape = true;
		}
		$context->content = $this->embedEcho($this->parseTalesExpression($value), $escape);
	}
	protected function tal_condition($value, $context) {
		$value = $this->parseTalesExpression($value);
		$context->prepend($this->embed('if('.$value.' !== false):'));
		$context->append($this->embed('endif'));
	}
	function getMetalFunctionName($file, $macro) {
		$file = realpath($file);
		//echo $file;
		$file = preg_replace('|[^a-zA-Z_]|', '_', $file);
		$macro = preg_replace('|[^a-zA-Z_]|', '_', $macro);
		//echo $file;
		return sprintf('microtal_macro_%s_%s',$file, $macro);
	}
	/*if ($name == 'else') {
					$context->sufix .= $this->embed('else:');
				}*/
	/*
	 * 		 		if ($name == 'define-macro') {
						
					} else if ($name == 'use-macro') {
						

	 */
	
	
	function var_replace($var) {
		return '($_r->resolve(\''.$var.'\'))';
	}
	function expression_replace_callback($matches) {
			$var = $this->var_replace($matches[1]);
		if(strlen($var)>0) {
			return "'.".$var.".'";
	}
	}
	function clean_callback($matches) {
		return $this->var_replace($matches[1]);
	}
	function var_replace_callback($matches) {
			$var = $this->var_replace($matches[1]);
		if(strlen($var)>0) {
			return '<?php echo '.$var.';?>';
	}
	}
	/**
	 * @param DOMNode $node
	 * @return string
	 */
	function compileNode($node) {
		switch($node->nodeType) {
			case XML_ELEMENT_NODE:
				return $this->compileElement($node);
				break;
			case XML_TEXT_NODE:
				return preg_replace_callback('|\$\{([^}]*?)}|', array($this,'var_replace_callback'), $node->nodeValue);
				break;
			case XML_COMMENT_NODE:
				return '';
				break;
			default:
				trigger_error("Unknown node type: ".$node->nodeType);
				return '';
				break;
		}
	}
	/**
	 * @param DOMElement $element 
	 * @return string
	 */
	protected function compileElement($element) {
		$context = new MicroTal_CompilingContext();
		if($element->hasChildNodes()) {
			foreach($element->childNodes as $node) {
				$context->content .= $this->compileNode($node);
			}
		}
		$dynamicAttributes = array();
		$staticAttributes = array();
		$context->omit = ($element->parentNode->nodeType === XML_DOCUMENT_NODE || $element->prefix === 'tal');
		$tal = $metal = array();
		if($element->hasAttributes()) {
			foreach ($element->attributes as  $attrNode) {
				if (!empty($attrNode->prefix)) {
					switch($attrNode->prefix) {
						case 'tal':
							$tal[$attrNode->name] = $attrNode->value;
							break;
						case 'metal':
							$metal[$attrNode->name] = $attrNode->value;
							break;
						default:
							$staticAttributes[$attrNode->prefix.':'.$attrNode->name] = $attrNode->value;
							break;
					}
				} else {
					$staticAttributes[$attrNode->name] = $attrNode->value;
				}
			}
		}
		
		if(!empty($tal)) {
		//	var_dump($tal);
		}
		if(!empty($tal['content'])) {
			$this->tal_content($tal['content'], $context);
		}
		if(!empty($tal['repeat'])) {
			$this->tal_repeat($tal['repeat'], $context);
		}
		if(!empty($tal['condition'])) {
			$this->tal_condition($tal['condition'], $context);
		}
		if(!empty($tal['replace'])) {
			$this->tal_replace($tal['replace'], $context);
		}
		if(array_key_exists('omit-tag', $tal)) {
			if (empty($tal['omit-tag'])) {
				$context->omit = true;
			} else {
				$context->omit = $tal['omit-tag'];
			}
		}
		if(!empty($tal['define'])) {
			$array = $this->getTalStatements($tal['define']);
			foreach($array as $statement) {
				//var_dump($statement);
			
			@list($item, $expression) = explode(' ', $statement, 2);
			$context->prependPhp('$_r->{\'%0\'} = %1',array($item, $this->parseTalesExpression($expression)));
			}
		}
		if (!empty($tal['attributes'])) {
			$array = $this->getTalStatements($tal['attributes']);
			foreach($array as $a) {
					list($item, $path) = explode(' ',trim($a),2);
					$dynamicAttributes[$item] = $path;
			}
		}
		/**
		 * if attribute value is equal 'false' it won't be included in tag at all
		 */
		$code ='';
		foreach($dynamicAttributes as $name => $path) {
			$code .= '$__attr_value = '.$this->parseTalesExpression($path).';';
			$code .= 'if ($__attr_value !== false) {';
			$code .= ' if ($__attr_value ===true) {$__attr_value = "'.$name.'";}';
			$code .= sprintf(' echo " %s";', $name.'=\"{$__attr_value}\"');
			$code .= "}";
			//dynamic attributes override static ones
			unset( $staticAttributes[$name]);
		}
		$attrString = $this->embed($code);

		foreach($staticAttributes as $name => $value) {
				$attrString .= sprintf(' %s="%s"', $name, $value);
		}

		if (isset($metal['use-macro'])) {
			list($f, $p) = $this->parseMetalPath($metal['use-macro']);
			$context->prepend('<?php $slots = array(); ob_start(); ?>');
			$context->append('<?php ob_end_clean(); ?>');
			$fn = $this->getMetalFunctionName($f, $p);
			$context->append('<?php if(!function_exists(\''.$fn.'\')) {if (!MicroTal::inc(\''.realpath($f).'\')) {trigger_error("Cannot resolve '.$metal['use-macro'].'");};} ?>');
			$code = $fn.'(new MicroTal_Variables($_r), $slots)';
			$context->append($this->embedEcho($code));
			//var_dump($context);
		}
		if (isset($metal['define-macro'])) {
			$fn = $this->getMetalFunctionName($this->__template,$metal['define-macro']);
			$context->prependPhp('if (!function_exists(\'%0\')) { function %0($_r, $slots) {ob_start();', $fn);
			$context->appendPhp('return ob_get_clean();}}');
		}
		if (isset($metal['define-slot'])) {
			$slot = $metal['define-slot'];
			$context->prependPhp('if (isset($slots[\'%0\'])) { echo $slots[\'%0\'];} else {', $slot);
			$context->appendPhp('}');
		}
		if (isset($metal['fill-slot'])) {
			$slot = $metal['fill-slot'];
			$context->prependPhp('ob_start()');
			$context->appendPhp('$slots[\'%0\'] = ob_get_clean()', $slot);
		}
		if ($context->omit === true) {
				$openTag = '';
				$closeTag = '';
		} else {
			if (in_array($element->nodeName, $this->selfClosing)) {
				$openTag = '<'.$element->nodeName.$attrString." />";
				$closeTag = '';
			} else {
				$openTag = '<'.$element->nodeName.$attrString.'>';
				$closeTag = '</'.$element->nodeName.">";
			}
			if ($context->omit !== false) {
				$value = $this->parseTalesExpression($context->omit);
				$prefix = $this->embed('if('.$value.' === false):');
				$sufix = $this->embed('endif;');
				$openTag = $prefix.$openTag.$sufix;
				if (!empty($closeTag)) {
					$closeTag = $prefix.$closeTag.$sufix;
				}
			}
		}
		return $context->prefix.$openTag.$context->content.$closeTag.$context->sufix;
	}
	public function compileString($string) {
		$string = '<?xml version="1.0" encoding="UTF-8"  ?>'.
			'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">'.
			'<html xmlns:tal="http://m/" xmlns:metal="http://m/">'.$string.'</html>';
		$dom = new DOMDocument();
		if($this->encoding__ != 'utf8') {
				$string = iconv($this->encoding__, 'utf8', $string);
		}
		$string = str_replace('&', '&amp;', $string);
		$dom->loadXML($string);
		$result = $this->compileElement($dom->documentElement);
		if($this->encoding__ != 'utf8') {
			$result = iconv('utf8', $this->encoding__, $result);
		}
		return $result;
	}
	function compileTemplate($input) {
		return $this->compileString($input);
	}
	
	function getCompiled() {
		$sourcePath = realpath($this->__template);
		//echo($this->compileTemplate(file_get_contents($sourcePath)));
		return $this->compileTemplate(file_get_contents($sourcePath));
	}
	public function execute($template) {
		if (substr($template, 0, 1) !== '/') {
			$tmpPath = $this->templateDir__ . '/' . $template;
		} else {
			$tmpPath = $template;
		}
		$sourcePath = realpath($tmpPath);
		if ($sourcePath	=== false) {
			throw new Exception("Template file $tmpPath not found");
		}
		$this->__template = $sourcePath;
			$compiledPath = $this->tempDir__.'/microtal_'.str_replace('/','__',$sourcePath).'.php';

			if(!$this->cacheEnabled__  ||
			!file_exists($compiledPath) ||
			filemtime($sourcePath) >  filemtime($compiledPath ) ||
			filemtime(__FILE__) > filemtime($compiledPath )) {
				file_put_contents($compiledPath, $this->compileTemplate(file_get_contents($sourcePath)));
			}
			$microtal = $this;
			//extract($this->__vars, EXTR_OVERWRITE|EXTR_REFS);
			$_r = new MicroTal_Variables($this->__vars);
		ob_start();
			require($compiledPath);
			$result = ob_get_clean();
			//$result = preg_replace('!\s+!'," ",$result);
			$result = preg_replace('![\n\r]\s?[\n\r]!m',"\n",$result);
		//$result = preg_replace('!\n+!m',"\n",$result);
		return $result;

	}
	static public function inc($path) {
		static $included = array();
		if (in_array($path, $included)) {
			return false;
		}
		$included[] = $path;
		$t = new MicroTal();
		$t->execute($path);
		return true;
	}
	public function executeString($string) {
		$code = $this->compileString($string);
		//extract($this->__vars, EXTR_OVERWRITE|EXTR_REFS);
		$icrotal = $this;
		$_r = new MicroTal_Variables($this->__vars);
		//echo '|||'.$code.'|||';
		//var_dump( '|||'.$code.'|||');
		ob_start();
		eval('?>'.$code);
		$result = ob_get_clean();
		return $result;
	}
	public function  __set($name, $value) {
			$this->__vars[$name] = $value;
	}
	protected function embed($expression) {
		$expression = (string) $expression;
		if (strlen(trim($expression)) > 0) {
			return '<?php '.$expression.' ?>';
		} else {
			return '';
		}
	}
	protected function embedEcho($expression, $escape = false) {
		$expression = (string) $expression;
		if ($escape) {
				$expression = 'microtal_escape('.$expression.')';
		}
		if (strlen(trim($expression)) > 0) {
			return '<?php echo '.$expression.";\n?>";
		} else {
			return '';
		}
	}
	/**
	 * Generates code that returns value of Tales path
	 * @param string $path
	 * @return string
	 */
	protected function parseTalesPath($path) {
		return $this->var_replace($path);
	}
	protected function callbackTalesPath($matches) {
		return $this->parseTalesPath($matches[1]);
	}
	public function callbackTalesPathInString($matches) {
			$result = sprintf("'.%s.'" ,$this->parseTalesPath($matches[1]));
		return $result;
	}
	/**
	 * Generates code that returns value of Tales expression
	 * @param string $expression
	 * @return string
	 */
	public function parseTalesExpression($inputExpression) {
			list($type, $expression) = $this->explodeRight(':', $inputExpression, 2);
		switch($type) {
			case 'not':
					$result = sprintf('(!(%s))', $this->parseTalesExpression($expression));
				return $result;
			break;
			case '':
				return $this->parseTalesPath($expression);
			break;
			case 'path':
				return $this->parseTalesPath($expression);
			break;
			case 'string':
					$this->pregInit($expression);
					$this->pregSave($expression, '|\$\{[^}]+?}|');
					$expression = "'".str_replace("'", "\\'", $expression)."'";
					$this->pregRestore($expression);
					$expression = preg_replace_callback('|\$\{([^}]+?)}|', array($this, "callbackTalesPathInString"), $expression);
				return $expression;
			break;
			case 'php':
				return preg_replace_callback('|\$\{([^}]*?)}|', array($this,'clean_callback'), $expression);
			break;
			default:
					trigger_error("Unknown TALES expression type: ".$type, E_USER_NOTICE);
		break;
	}
	}
		/**
		 * Explode wrapper that ensures that array of $limit elements is always
		 * returned, filling the array from beggining with $default
		 * @param string $delimiter
		 * @param string $string
		 * @param integer $limit
		 * @param string $default
		 * @return string
		 */
	protected function explodeRight($delimiter, $string, $limit, $default ='') {
			$parts = explode($delimiter, $string, $limit);
		while(count($parts) < $limit) {
				array_splice($parts, 0, 0, $default);
		}
		return $parts;
	}
	function pregInit(&$expression) {
			$this->pregItems = array();
	}
	function pregSave(&$expression, $regex) {
			$expression = preg_replace_callback($regex, array($this, "pregSave_"), $expression);
	}
	function pregSave_($matches) {
		static $uniq=0;
		$uniq++;
			$placeholder = '#preg_placeholder##'.$uniq.'#';
			$this->pregItems[$placeholder] = $matches[0];
		return $placeholder;
	}
	function pregRestore_($matches) {
			$result = $this->pregItems[$matches[0]];
			unset ($this->pregItems[$matches[0]]);
		return $result;
	}
	function pregRestore(&$expression) {
			$expression = preg_replace_callback('|#preg_placeholder##[^#]+?#|', array($this, "pregRestore_"), $expression);
	}
	/**
	 * Breaks METAL path into file name and macro name;
	 * @param string $path
	 * @return array
	 */
	function parseMetalPath($path) {
			$items = explode('/', $path);
			$tmp = $this->templateDir__;
			if (!empty($items[0])) {
				return array($this->__template, implode('__', $items));
			}
		while(true) {
			if (is_file($tmp)) {
				return array($tmp, implode('__', $items));
			}
			if (is_dir($tmp)) {
					list($dir) = array_splice($items, 0, 1);
					$tmp = $tmp . DIRECTORY_SEPARATOR . $dir;
			continue;
			}
		break;
		}
		throw new Exception('Macro '.$path.' Can\'t be found');
	}
		/**
		 * implodes using DIRECTORY_SEPARATOR as separator
		 * @param array $paths array of file path fragments
		 * @return string
		 */
	function pathImplode($paths) {
		return implode(DIRECTORY_SEPARATOR, $paths);
	}
		/**
		 * Returns path for file that will store compiled template
		 * @param string $inputfile
		 * @return string
		 */
	function getCompiledFileName($inputfile) {
			$sourcePath = realpath($inputfile);
		return $this->pathImplode(array($this->tempDir__, 'microtal_', strtr('/\\','__',$sourcePath),'.php'));
	}
		/**
		 * Creates function by wrapping suplied code, quite similar to create_function.
		 * Prevents duplicate definition error.
		 * @param string $name function name
		 * @param string $args list of function arguments
		 * @param string $code function body
		 * @return string
		 */
	protected function createFunction($name, $args, $code) {
			$result = sprintf('if(!function_exists("%s")) {'."\n", $name);
			$result .= sprintf('function %s(%s) {'."\n", $name, $args);
			$result .= $code."\n}\n}\n";
		return $result;
	}
	protected function getTalStatements($value) {
			$statements = explode(';', $value);
			if (count($statements) >= 3) {
		// concat back false positives (";;")
			foreach($statements as $k => $v) {
				if (isset($array[$k-1]) &&	isset($array[$k-2])) {
					if ($array[$k-1] === '') {
						$array[$k-2] .= $array[$k];
						unset($array[$k-1],$array[$k]);
					}
				}
			}
		}
		return array_reverse($statements);
	}
		/**
		 * Check if string begins with given keyword.
		 * If it does, keyword is removed from string, and function returns true.
		 * Otherwise returns false;
		 * @param string $expression
		 * @param string $keyword
		 * @return boolean
		 */
	protected function checkKeyword(&$expression, $keyword) {
			$keyLength = mb_strlen($keyword);
			if(mb_substr($expression, 0, $keyLength) == $keyword) {
		//Fixme : should trim just from left side
				$expression = trim(substr($expression, $keyLength));
			return true;
		}
		return false;
	}
/**
 * Setters
 */
	public function setTemplateDir($templateDir) {
			$this->templateDir__ = $templateDir;
	}

	public function setRelativeTemplateDir($templateDir) {
			$this->templateDir__ = $_SERVER['DOCUMENT_ROOT'].'/'.$templateDir;
	}

	public function setTempDir($tempDir) {
			$this->tempDir__ = $tempDir;
	}
		/**
		 * @param string $encoding
		 */
	public function setEncoding($encoding) {
			$this->encoding__ = $encoding;
	}
	/**
	 * @param boolean $cacheEnabled
	 */
	public function setCacheEnabled($cacheEnabled) {
			$this->cacheEnabled__ = $cacheEnabled;
	}
	public function smarty_prefilter_callback($parts) {
		return '{php} $_r = new MicroTal_Variables($this->_tpl_vars); ?>'.$this->compileString($parts[1]).'<?php {/php}';
	}
	public function smarty_prefilter($tpl_source, &$smarty)	{
		$result = preg_replace_callback("|<microtal>(.*?)</microtal>|U", array($this,'smarty_prefilter_callback'), $tpl_source);
		return $result;
	}
	public function registerWithSmarty(&$smarty) {
		$smarty->register_prefilter(array($this, 'smarty_prefilter'));
	}


}
	/**
	 * Used to concat many lines of code. Appends ";" and "\n".
	 * %[0-9] are replaced with variables.
	 */

/**
 * Internal function called from compiled templates
 * @param object|array $start
 * @param array $path
 * @return mixed
 */
/*function microtal_resolve($start, $path) {
*/
function microtal_escape($string) {
	static $from = array( '&', '<', '>', '"');
	static $to = array( '&amp;','&lt;', '&gt;', '&quot;');
	return str_replace($from, $to, $string);
}

function microtal_unescape($string) {
	static $to = array('&lt;', '&gt;', '&quot;', '&amp;');
	static $from = array('<', '>', '"', '&');
	return str_replace($from, $to, $string);
}

class MicroTal_Variables {
	protected $_variables;
	function __construct($variables = array()) {
		if ($variables instanceof MicroTal_Variables) {
			$this->_variables = $variables->_variables;
		} else {
			$this->_variables = $variables;
		}
		$this->_variables['repeat'] = new ArrayObject();
	}
	function resolve($path) {
		$path = explode("/", $path);
		$current = array_shift($path);
		if (array_key_exists($current, $this->_variables)) {
			$current = $this->_variables[$current];
		} else {
			return false;
		}
		while(!empty($path)) {
			$next = array_shift($path);
			if (is_array($current) || $current instanceof ArrayObject) {
				if (isset($current[$next])) {
					$current=$current[$next];
					continue;
				}
			} else if (is_object($current)) {
				if (property_exists($current, $next)) {
					$current = $current->$next;
					continue;
				} else if (method_exists($current, $next) || is_callable(array($current,$next))) {
					if(empty($path)) {
						return $current = $current->$next();
					} else {
						return call_user_func_array(array($current, $next), $path);
					}
				}
			}
			return false;
		}
		return $current;
	}
	function  __set($name,  $value) {
		$this->_variables[$name] = $value;
	}
	function  __get($name) {
		return $this->_variables[$name];
	}
	function  getRepeat() {
		return $this->_variables['repeat'];
	}
}

